Fix two OOM error-path defects in the CPU PNG encoder - #9635
Conversation
Both are only reachable when an allocation fails, and both make that failure worse than it needs to be. 1. `torch_png_write_data` assigned the result of `realloc` straight back to `p->buffer`. `realloc` does not free the original block when it returns NULL, so this discarded the only pointer to it. The `png_error` -> `longjmp` cleanup path then sees `buf_info.buffer == nullptr` and skips the `free`, leaking whatever had been encoded so far (up to roughly the size of the finished PNG). The leak does not grow without bound -- each leaked block reduces what the next encode can obtain, so failures happen progressively earlier -- but it permanently destroys the process's ability to encode. Under a fixed memory cap, encode capacity collapsed to zero within three failed calls and never recovered; with the temporary pointer it holds steady indefinitely instead. `realloc(NULL, n)` is equivalent to `malloc(n)`, so the first-call branch is no longer needed. 2. `png_create_write_struct` and `png_create_info_struct` were not checked for NULL. Every subsequent libpng call NULL-guards its `png_ptr` and returns early, so on allocation failure `encode_png` ran to completion and returned an empty tensor: a silent, zero-byte "PNG" with no error raised. `decode_png` already handles the mirror case; this brings the encoder in line with it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
🔗 Helpful Links🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/vision/9635
Note: Links to docs will display an error until the docs builds have been completed. This comment was automatically generated by Dr. CI and updates every 15 minutes. |
|
Hi @fjankovi! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
Two defects in
encode_png, both reachable only when an allocation fails, and both of which make that failure worse than it needs to be. They share a precondition, so they're fixed together.1.
reallocresult assigned back to the pointer it reallocatesreallocdoes not free the original block when it returnsNULL, but the only pointer to that block has just been overwritten. Thepng_error→longjmpcleanup path then readsbuf_info.buffer == nullptrand skips itsfree, so everything encoded so far is leaked — up to roughly the size of the finished PNG.The compiler makes this unconditional; there's no window in which the old pointer survives:
The leak does not grow without bound — each leaked block reduces what the next encode can obtain, so subsequent failures occur progressively earlier and the series converges. What it does instead is permanently destroy the process's ability to encode. Measured under a fixed
RLIMIT_AScap, encoding 1024×1024 RGB noise in a loop:So the practical effect is that one failed encode turns a recoverable, transient OOM into a permanent one. With the fix, capacity holds flat indefinitely and every failure is fully recoverable.
Since
realloc(NULL, n)is equivalent tomalloc(n)(C99 7.22.3.5), the first-call branch is no longer needed and the whole thing collapses to a temporary plus a commit-on-success.2.
png_create_write_struct/png_create_info_structnot checked forNULLEvery subsequent libpng call NULL-guards its
png_ptrand returns early, so when the create call fails,encode_pngruns all the way to completion and returns an empty tensor — a silent, zero-byte "PNG" with no error raised. Reproduced by interposing aNULL-returningpng_create_write_struct.decode_pngalready handles the mirror case (decode_png.cpp:40-48); this brings the encoder in line with it, reusing the same message wording.Validation
There's no way to force an allocation failure from the Python test suite, so this isn't covered by a new unit test. It was validated against a faithful reduction of the function built on real libpng 1.6.43, with a deterministic allocation-failure shim:
Direct leak of 98489 byte(s) in 1 object(s)intorch_png_write_data. After: clean.-Wall -Wextra: happy path (output decodes back to the correct dimensions),reallocfailure mid-encode,reallocfailure on the first call,NULLcreate (now raises instead of returning 0 bytes), zero-sized dimensions (the reachablelongjmppath, stillInvalid IHDR data), and invalid arguments.clang-format18.1.3 clean, matching the.pre-commit-config.yamlpin.The happy path is unchanged — these are error-path-only edits.
🤖 Generated with Claude Code